/*
  A node in the mindmap.
  
  (c) copyright
  
  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General
  Public License along with this library; if not, write to the
  Free Software Foundation, Inc., 59 Temple Place, Suite 330,
  Boston, MA  02111-1307  USA
 */

package dodo.mindmap;

import geomerative.RFont;
import geomerative.RG;
import geomerative.RPoint;
import geomerative.RShape;
import geomerative.RStyle;

import java.awt.Color;
import java.awt.Component;
import java.awt.TextArea;
import java.awt.TextField;
import java.awt.event.MouseEvent;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.NoSuchElementException;
import java.util.TreeSet;
import java.util.Vector;

import javax.swing.JComponent;
import javax.swing.JPanel;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.xerces.parsers.DOMParser;
import org.w3c.dom.Document;
import org.w3c.dom.*;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import dodo.ui.DTextArea;
import dodo.ui.DTextField;

import megamu.shapetween.Tween;

import processing.core.PApplet;
import processing.core.PFont;
import processing.core.PConstants;

/**
 * Node is the graphical containter for a mindmap node.
 * It handles the animation and drawing of the node on the map.
 *  
 * @author David Kadish
 */
public class Node implements PConstants {

	MApplet parent;
	Mindmap map;
	int id;
	RPoint velocity, position, size;
	RStyle shapeStyle, textStyle;
	Color color, textcolor;
	float mass, charge;
	String title, content;
	Drawable shape;
	Vector<Link> links;
	Vector<NodeButton> buttons;
	Vector<RShape> formElements;
	NodeState state;
	Tween animation;
	
	DTextArea title_field;
	DTextArea content_field;
	
	protected PFont font;
	protected static final RPoint FOCUS_SIZE = new RPoint(300,300);
	protected static final float RECTANGLE_ARC = 15;
	protected static final float BUTTON_MARGIN = 10;
	protected static final float BUTTON_SIZE = 100;

	public final String VERSION = "0.1.0";
	
	public Node(Mindmap map, RPoint position, RPoint size, float mass, RStyle shapeStyle, RStyle textStyle, String title, String content){
		this(map, 0, position, size, mass, shapeStyle, textStyle, title, content);
	}
	
	/**
	 * The Node constructor
	 * 
	 * @param parent The parent PApplet object
	 * @param position The vector to the centre of the node
	 * @param color The color of the node background
	 * @param textcolor The color of the node text
	 * @param title The title text. This text will appear when the node is viewed in the standard view.
	 * @param content The node details will be viewed when the node is zoomed and has focus.
	 */
	public Node(Mindmap map, int id, RPoint position, RPoint size, float mass, RStyle shapeStyle, RStyle textStyle, String title, String content ) {
		this.id = id;
		this.parent = map.parent;
		this.map = map;
		this.position = position;
		this.velocity = new RPoint(0,0);
		this.size = size;
		this.mass = mass;
		this.shapeStyle = shapeStyle;
		this.textStyle = textStyle;
		this.color = new Color(shapeStyle.fillColor);
		this.textcolor = new Color(textStyle.fillColor);
		this.title = title;
		this.content = content;
		this.state = NodeState.BIRTH;
		this.charge = 100;
		
		this.links = new Vector<Link>();
		this.buttons = new Vector<NodeButton>();
		this.formElements = new Vector<RShape>();
		
		this.shape = new Circle(this.position.x, this.position.y, this.size.x);
		this.animation = new Tween(parent, 10, Tween.FRAMES);
		this.animation.start();
		
		// Set up the fonts
		this.font = this.parent.createFont("DejaVu Sans", 20, true);
		this.parent.textFont(this.font);
		
		this.parent.registerMouseEvent(this);
		this.parent.rectMode(CENTER);
		
		RG.init(parent);
		
	}
	
	public Node(Mindmap map, RPoint position, RPoint size, float mass, RStyle shapeStyle, RStyle textStyle) {
		this(map, position, size, mass, shapeStyle, textStyle, "", "");
	}
	
	public int getColor(Color color){
		switch (state) {
		case BLURRED:
			return color.darker().getRGB();

		default:
			return color.getRGB();
		}
	}
	
	public float getCharge() {
		return charge;
	}

	public void setCharge(float charge) {
		this.charge = charge;
	}
	
	public Node getParent(){
		//TODO: THIS ONLY WORKS FOR NEW NODES
		for(Link l: links){
			if(l.k == 1 && l.to == this)
				return l.getTo(this);
		}
		return null;
	}

	public RPoint getSize() {
		switch (state) {
		case FOCUSED:
			return FOCUS_SIZE;
		case CONCEPTION:
			return FOCUS_SIZE;

		default:
			return new RPoint(PApplet.lerp(0,size.x,animation.position()), PApplet.lerp(0, size.y, animation.position()));
		}
	}

	public void setSize(float charge) {
		this.charge = charge;
	}

	/**
	 * Draws the node onto the screen
	 * 
	 * Draw prints the background shape and then lays out the text on the screen.
	 */
	public void draw(){
		switch(state){
		case CONCEIVING:
			state = NodeState.CONCEPTION;
			RPoint rect_pos = new RPoint(position);
			rect_pos.sub(new RPoint(getSize().x/2, getSize().y/2));
			
			shape = new RoundRectangle(rect_pos, FOCUS_SIZE, RECTANGLE_ARC);
			createForm();
			break;
			
		case CONCEPTION:
			drawShape();
			drawForm();
			break;
			
		case GESTATION:
			state = NodeState.BIRTH;
			break;
		
		case BIRTH:
			parent.fill(getColor(color));
			
			double width = PApplet.lerp(0, (float) size.x, animation.position());
			double height = PApplet.lerp(0, (float) size.y, animation.position());
			shape.setFrame(position.x - width/2, position.y - height/2, width, height );
			shape.draw(parent);
			
			this.parent.fill(getColor(textcolor));
			this.parent.textAlign(CENTER, CENTER);
			
			if(!animation.isTweening())
				state = NodeState.LIFE;
			
			break;
		case LIFE:
			drawShape();
			drawTitle();
			break;
		case FOCUSING:
			state = NodeState.FOCUSED; //TODO: Replace this with animation
			createButtons();
			
			break;
		case UNFOCUSING:
			state = NodeState.LIFE; //TODO: Replace this with animation
			buttons.clear();
			
			break;
		case FOCUSED:
			drawShape();
			drawFullText();
			drawButtons();
			break;
		case BLURRING:
			state = NodeState.BLURRED; //TODO: Replace this with animation
			
			break;
		case BLURRED:
			drawShape();
			drawTitle();
			break;
		case UNBLURRING:
			state = NodeState.LIFE; //TODO: Replace this with animation
			
			break;
		case DEATH:

			break;
		default:
			break;
		}
	}
	
	public Vector<Node> getCollisions(Vector<Node> nodes){
		Vector<Node> collisions = new Vector<Node>();
		for(Node n: nodes){
			RPoint d = new RPoint(position);
			d.sub(n.position);
			if(d.norm() < (getSize().x + n.getSize().x)/2){
				collisions.add(n);
			}
		}
		collisions.remove(this);
		return collisions;
	}
	
	public boolean clicked(float f, float g){
		switch (state) {
		case FOCUSED:
			return shape.contains(f, g) || getClicked(new RPoint(f,g)) != null;
		case BLURRED:
			return false;
		default:
			return shape.contains(f, g);
		}
	}
	
	public boolean clicked(RPoint p){
		return clicked(p.x,p.y);
	}
	
	public void mouseEvent(MouseEvent e){
		RPoint p = new RPoint(e.getX(), e.getY());
		switch (e.getID()) {
		case MouseEvent.MOUSE_CLICKED:
			if( clicked(p.x, p.y)){
				switch (state) {
				case FOCUSED:
					if(getClicked(p) != null){
						switch (getClicked(p).action) {
						case CANCEL:
							map.doUnfocus();
							break;
						case CONNECT:
							map.doLinking(this);
							break;
						case NEW:
							RPoint pt = new RPoint(position); 
							pt.add(new RPoint(200,200));
							map.doMakeChildNode(this, new Node(map, pt, new RPoint(150,150), 100, NodeStyle.node, NodeStyle.nodeText));
							break;
						default:
							break;
						}
					}
					break;
				default:
					break;
				}
			}else{
				switch (state) {
				case CONCEPTION:
					if(!clicked(p)){
						// Populate the node with the text
						title = title_field.getText();
						content = content_field.getText();
						
						// Remove the form
						map.parent.remove(title_field);
						map.parent.remove(content_field);
						
						title_field = null;
						content_field = null;
						
						// Change the shape
						shape = new Circle(position.x, position.y, size.x);
						
						state = NodeState.GESTATION;
					}
					break;

				default:
					break;
				}
			}
			break;

		default:
			break;
		}
	}
	
	public RPoint vectorToNode(Node n){
		RPoint unit = new RPoint(position);
		unit.sub(n.position);
		float dist = unit.norm();
		
		unit.normalize();
		unit.scale(dist - (size.x + n.size.x)/2);
		
		return unit;
	}
	
	public float getMass(){
		return PApplet.lerp(1, mass, animation.position());
	}
	
	public NodeButton getClicked(RPoint p){
		for( NodeButton b: buttons ){
			if( b.clicked(p) )
				return b;
		}
		return null;
	}
	
	public void updateFromXML(org.w3c.dom.Node idea){
		
        id = Integer.parseInt(idea.getAttributes().getNamedItem("pk").getNodeValue());
        NodeList children = idea.getChildNodes();
        for(int i=0; i < children.getLength(); i++){
        	if(children.item(i).getAttributes().getNamedItem("name").getNodeValue() == "size"){
        		size.x = Float.valueOf(children.item(i).getNodeValue());
        		size.y = size.x;
        	}
        }
	}
	
	public void updateFromXML(String xml){
		DOMParser parser = new DOMParser();
        try {
			parser.parse(new org.xml.sax.InputSource(new java.io.StringReader(xml)));
	        Document doc = parser.getDocument();

	        updateFromXML(doc.getElementsByTagName("object").item(0));
		} catch (SAXException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public static void sendCreateNode(Node n){

		HttpClient httpclient = new DefaultHttpClient();
		HttpPost httppost = new HttpPost("http://172.16.1.5:80/ideavolution/node/new");
		String response;
		
		ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
		params.add(new BasicNameValuePair("title", n.getTitle()));
		params.add(new BasicNameValuePair("text", n.getContent()));
		params.add(new BasicNameValuePair("size", String.valueOf(n.getSteadySize().x)));
		
		if(n.getParent() != null)
			params.add(new BasicNameValuePair("parent",String.valueOf(n.getParent().getId())));

		try {
			UrlEncodedFormEntity encoded_params = new UrlEncodedFormEntity(params);
			httppost.setEntity(encoded_params);
			ResponseHandler<String> responseHandler = new BasicResponseHandler();
			response = httpclient.execute(httppost, responseHandler);
			n.updateFromXML(response);
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ClientProtocolException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public static Node pathCollision(final Node moving, final RPoint from, RPoint to, Vector<Node> nodes){
		
		RPoint path = new RPoint(from);
		from.sub(to);
		final RPoint parallel = new RPoint(path);
		parallel.normalize();
		final RPoint norm = new RPoint(-parallel.y, parallel.x);
		Circle end_shape = new Circle(to.x, to.y, moving.getSize().x);

		TreeSet<Node> collisions = new TreeSet<Node>(new Comparator<Node>(){
			public int compare(Node n1, Node n2){
				RPoint p1 = new RPoint(collisionPoint(n1));
				RPoint p2 = new RPoint(collisionPoint(n2));
				
				RPoint d1 = new RPoint(p1);
				RPoint d2 = new RPoint(p2);
				
				d1.sub(from);
				d2.sub(from);
				
				if( d1.norm() < d2.norm() )
					return 1;
				else if( d1.norm() > d2.norm())
					return -1;
				else
					return 0;
			}
			
			@SuppressWarnings("unused")
			public boolean equals(Node n1, Node n2){
				RPoint p1 = new RPoint(collisionPoint(n1));
				RPoint p2 = new RPoint(collisionPoint(n1));
				
				RPoint d1 = new RPoint(p1);
				RPoint d2 = new RPoint(p2);
				
				d1.sub(from);
				d2.sub(from);

				if( d1.norm() == d2.norm() )
					return true;
				else
					return false;
			}
			
			private RPoint collisionPoint(Node n){
				// Find normal distance to line
				// Find vector along axis to collision point
				// Find full vector
				RPoint diff = new RPoint(n.position);
				diff.sub(from);
				
				float proj_norm = diff.mult(norm);
				float p_dist = PApplet.sqrt(PApplet.sq((moving.getSize().x + n.getSize().x)/2) - PApplet.sq(proj_norm));
				diff.scale(-1);
				float translate = diff.mult(parallel) - p_dist;
				
				RPoint p = new RPoint(parallel);
				p.scale(translate);
				
				return p;
			}
			
		});
		
		for(Node n:nodes){
			// Check the distance to the line
			if( n != moving ){
				float radius_sum = (moving.getSize().x + n.getSize().x)/2;
				
				RPoint dFrom = new RPoint(n.position);
				RPoint dTo = new RPoint(n.position);
				dFrom.sub(from);
				dTo.sub(to);
				
				float from_dist = dFrom.norm();
				float to_dist = dTo.norm();
				float motion_dist = dFrom.mult(parallel);
				
				boolean in_range = PApplet.abs(motion_dist) < radius_sum;
				RPoint nParallel = new RPoint(parallel);
				nParallel.scale(-1);
				boolean in_angle = dFrom.angle(parallel) < HALF_PI && dTo.angle(nParallel) < HALF_PI;
				boolean in_shapes = PApplet.abs(from_dist) < radius_sum || PApplet.abs(to_dist) < radius_sum;
				
				if( (in_range && in_angle) || in_shapes )
					collisions.add(n);
			}
		}
		
		try {
			return collisions.first();
		} catch (NoSuchElementException e) {
			return null;
		}
	}
	
	protected void drawShape(){
		parent.fill(getColor(color));
		
		shape.setFrame(position.x - getSize().x/2, position.y - getSize().y/2, getSize().x, getSize().y );
		shape.draw(parent);
	}
	
	protected void drawButtons(){
		for( NodeButton b: buttons ){
			b.draw();
		}
	}
	
	protected void drawForm(){
		for(RShape s: formElements){
			s.draw();
		}
	}

	
	protected void createForm(){
		int font_size = 14;
		
		RFont f = new RFont("lucon.ttf",14,RFont.LEFT);
		
		RPoint pos = new RPoint(shape.getMinX(),shape.getMinY());//position);
		pos.add(new RPoint(RECTANGLE_ARC, RECTANGLE_ARC + font_size/2));
		
		int width = (int) (FOCUS_SIZE.x - 2 * RECTANGLE_ARC);
		
		RShape title_label = f.toShape("Title");
		title_label.translate(pos);
		title_label.setStyle(NodeStyle.buttonText);
		formElements.add(title_label);
		
		pos.add(new RPoint(0,15));
		title_field = new DTextArea(pos, new RPoint(width, 20));
		map.parent.add(title_field);
		pos.add(new RPoint(0,40));

		RShape content_label = f.toShape("Content");
		content_label.translate(pos);
		content_label.setStyle(NodeStyle.buttonText);
		formElements.add(content_label);
		
		pos.add(new RPoint(0,15));
		content_field = new DTextArea(pos,new RPoint(width, shape.getMaxY() - pos.y - RECTANGLE_ARC)); 
		map.parent.add(content_field);
		
	}
	
	protected void createButtons(){
		RFont f = new RFont("lucon.ttf",20,RFont.CENTER);
		
		float innerRad = getSize().x / 2 + BUTTON_MARGIN;
		float outerRad = getSize().x / 2 + BUTTON_MARGIN + BUTTON_SIZE;
		
		float start_angle = HALF_PI; // The angle for the first button to start at
		float separation_angle = BUTTON_MARGIN / innerRad;
		float arc = 2 * PI / 3 - separation_angle;
		
		// Hack the start angle to be what we want
		start_angle *= -1;
		start_angle += HALF_PI + arc/2;
		
		String[] strings = {"New Idea", "Cancel", "Connect"};
		NodeButtonAction[] actions = {NodeButtonAction.NEW, NodeButtonAction.CANCEL, NodeButtonAction.CONNECT};
		
		for(int i=0; i < 3; i++){
			NodeButton b = new NodeButton(this, position, start_angle, NodeButton.circularButton(innerRad, outerRad, arc), NodeButton.text(strings[i], f, (innerRad + outerRad)/2), actions[i]);
			buttons.add(b);
			start_angle += arc + separation_angle;
		}
		
	}
	
	protected void drawTitle(){
		parent.fill(getColor(textcolor));
		parent.textAlign(CENTER, CENTER);
		if(map.base == this)
			parent.textSize(18);
		else
			parent.textSize(14);
		parent.text(
				title, position.x, position.y,
				(float) shape.getTextBoxBounds().getWidth(),
				(float) shape.getTextBoxBounds().getHeight()
		);
	}
	
	protected void drawFullText(){
		float width = (float) shape.getTextBoxBounds().getWidth();
		float height = (float) shape.getTextBoxBounds().getHeight();
		
		parent.fill(getColor(textcolor));
		parent.textAlign(CENTER, TOP);
		parent.text(title, position.x, position.y - height * 0.4f, width, height * 0.2f);
		parent.textSize(14);
		parent.text(content, position.x, position.y + height * 0.1f, width, height * 0.8f);
		parent.textSize(20);
		
	}
	
	protected RPoint rotateVectorCCW(RPoint vector, float angle){
		float cos = PApplet.cos(angle);
		float sin = PApplet.sin(angle);
		return new RPoint(vector.x * cos - vector.y * sin, vector.x * sin + vector.y * cos);
	}
	
	protected RPoint rotateVectorCW(RPoint vector, float angle){
		float cos = PApplet.cos(angle);
		float sin = PApplet.sin(angle);
		return new RPoint(vector.x * cos + vector.y * sin, - vector.x * sin + vector.y * cos);
	}
	
	public byte[] getMD5(){
		String input = "" + id + title + content + size.x;
		
		try {
			MessageDigest md5 = MessageDigest.getInstance("MD5");

			return md5.digest(input.getBytes());
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		}
	}
	
	public String getTitle() {
		if( title_field != null )
			return title_field.getText();
		else
			return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getContent() {
		if( content_field != null )
			return content_field.getText();
		else
			return content;
	}

	public void setContent(String content) {
		this.content = content;
	}
	
	public RPoint getSteadySize(){
		return size;
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}
	
	public String dbRequest(String field){
		HttpClient httpclient = new DefaultHttpClient();
		HttpPost httppost = new HttpPost(map.baseurl + "/ideavolution/node/" + id + "/" + field + "/");
		ResponseHandler<String> responseHandler = new BasicResponseHandler();
		try {
			return httpclient.execute(httppost, responseHandler);
		} catch (ClientProtocolException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}
	
	public Node getDBParent(){
		String pid = dbRequest("parent_id");
		if( pid != null && !pid.equals("None") ){
			int parent_id = Integer.parseInt(pid);
			if(map.nodes.containsKey(parent_id))
				return map.nodes.get(parent_id);
		}
		return null;
	}
	
	public String getDBTitle(){
		return dbRequest("title");
	}
	
	public String getDBContent(){
		return dbRequest("text");
	}
	
	public float getDBSize(){
		String s = dbRequest("size");
		
		return Float.parseFloat(s);
	}
	/**
	 * return the version of the library.
	 * 
	 * @return String
	 */
	public String version() {
		return VERSION;
	}
}
